import sys
from flask import Flask, jsonify, request, send_file
from flask_cors import CORS
import yaml

app = Flask(__name__)
cors = CORS(app, resources={r"/*": {"origins": "*"}})

import os
import time
import uuid
from datetime import datetime
from typing import Dict, List, Text

try:
    import allure

    ALLURE = allure
except ModuleNotFoundError:
    ALLURE = None

from loguru import logger

from httprunner.client import HttpSession
from httprunner.config import Config
from httprunner.exceptions import ParamsError, ValidationFailure
from httprunner.loader import load_project_meta
from httprunner.models import (
    ProjectMeta,
    StepResult,
    TConfig,
    TestCaseInOut,
    TestCaseSummary,
    TestCaseTime,
    VariablesMapping,
)
from httprunner.parser import Parser
from httprunner.utils import LOGGER_FORMAT, merge_variables, ga4_client


class SessionRunner(object):
    config: Config
    teststeps: List[object]  # list of Step

    parser: Parser = None
    session: HttpSession = None
    case_id: Text = ""
    root_dir: Text = ""
    thrift_client = None
    db_engine = None

    __config: TConfig
    __project_meta: ProjectMeta = None
    __export: List[Text] = []
    __step_results: List[StepResult] = []
    __session_variables: VariablesMapping = {}
    __is_referenced: bool = False
    # time
    __start_at: float = 0
    __duration: float = 0
    # log
    __log_path: Text = ""

    def __init(self):
        self.__config = self.config.struct()
        self.__session_variables = self.__session_variables or {}
        self.__start_at = 0
        self.__duration = 0
        self.__is_referenced = self.__is_referenced or False

        self.__project_meta = self.__project_meta or load_project_meta(
            self.__config.path
        )
        self.case_id = self.case_id or str(uuid.uuid4())
        self.root_dir = self.root_dir or self.__project_meta.RootDir
        self.__log_path = os.path.join(self.root_dir, "logs", f"{self.case_id}.run.log")

        self.__step_results = self.__step_results or []
        self.session = self.session or HttpSession()
        self.parser = self.parser or Parser(self.__project_meta.functions)

    def with_session(self, session: HttpSession) -> "SessionRunner":
        self.session = session
        return self

    def get_config(self) -> TConfig:
        return self.__config

    def set_referenced(self) -> "SessionRunner":
        self.__is_referenced = True
        return self

    def with_case_id(self, case_id: Text) -> "SessionRunner":
        self.case_id = case_id
        return self

    def with_variables(self, variables: VariablesMapping) -> "SessionRunner":
        self.__session_variables = variables
        return self

    def with_export(self, export: List[Text]) -> "SessionRunner":
        self.__export = export
        return self

    def with_thrift_client(self, thrift_client) -> "SessionRunner":
        self.thrift_client = thrift_client
        return self

    def with_db_engine(self, db_engine) -> "SessionRunner":
        self.db_engine = db_engine
        return self

    def __parse_config(self, param: Dict = None) -> None:
        # parse config variables
        self.__config.variables.update(self.__session_variables)
        if param:
            self.__config.variables.update(param)
        self.__config.variables = self.parser.parse_variables(self.__config.variables)

        # parse config name
        self.__config.name = self.parser.parse_data(
            self.__config.name, self.__config.variables
        )

        # parse config base url
        self.__config.base_url = self.parser.parse_data(
            self.__config.base_url, self.__config.variables
        )

    def get_export_variables(self) -> Dict:
        # override testcase export vars with step export
        export_var_names = self.__export or self.__config.export
        export_vars_mapping = {}
        for var_name in export_var_names:
            if var_name not in self.__session_variables:
                raise ParamsError(
                    f"failed to export variable {var_name} from session variables {self.__session_variables}"
                )

            export_vars_mapping[var_name] = self.__session_variables[var_name]

        return export_vars_mapping

    def get_summary(self) -> TestCaseSummary:
        """get testcase result summary"""
        start_at_timestamp = self.__start_at
        start_at_iso_format = datetime.utcfromtimestamp(start_at_timestamp).isoformat()

        summary_success = True
        for step_result in self.__step_results:
            if not step_result.success:
                summary_success = False
                break

        return TestCaseSummary(
            name=self.__config.name,
            success=summary_success,
            case_id=self.case_id,
            time=TestCaseTime(
                start_at=self.__start_at,
                start_at_iso_format=start_at_iso_format,
                duration=self.__duration,
            ),
            in_out=TestCaseInOut(
                config_vars=self.__config.variables,
                export_vars=self.get_export_variables(),
            ),
            log=self.__log_path,
            step_results=self.__step_results,
        )

    def merge_step_variables(self, variables: VariablesMapping) -> VariablesMapping:
        # override variables
        # step variables > extracted variables from previous steps
        variables = merge_variables(variables, self.__session_variables)
        # step variables > testcase config variables
        variables = merge_variables(variables, self.__config.variables)

        # parse variables
        return self.parser.parse_variables(variables)

    def run_step(self, step):
        """run teststep, step maybe any kind that implements IStep interface

        Args:
            step (Step): teststep

        """
        logger.info(f"run step begin: {step.name()} >>>>>>")

        # run step
        for i in range(step.retry_times + 1):
            try:
                if ALLURE is not None:
                    with ALLURE.step(f"step: {step.name()}"):
                        step_result: StepResult = step.run(self)
                else:
                    step_result: StepResult = step.run(self)
                break
            except ValidationFailure:
                if i == step.retry_times:
                    raise
                else:
                    logger.warning(
                        f"run step {step.name()} validation failed,wait {step.retry_interval} sec and try again"
                    )
                    time.sleep(step.retry_interval)
                    logger.info(
                        f"run step retry ({i + 1}/{step.retry_times} time): {step.name()} >>>>>>"
                    )

        # save extracted variables to session variables
        self.__session_variables.update(step_result.export_vars)
        # update testcase summary
        self.__step_results.append(step_result)

        logger.info(f"run step end: {step.name()} <<<<<<\n")

    def test_start(self, param: Dict = None) -> "SessionRunner":
        """main entrance, discovered by pytest"""
        ga4_client.send_event("test_start")
        print("\n")
        self.__init()
        self.__parse_config(param)

        if ALLURE is not None and not self.__is_referenced:
            # update allure report meta
            ALLURE.dynamic.title(self.__config.name)
            ALLURE.dynamic.description(f"TestCase ID: {self.case_id}")

        logger.info(
            f"Start to run testcase: {self.__config.name}, TestCase ID: {self.case_id}"
        )

        logger.add(self.__log_path, format=LOGGER_FORMAT, level="DEBUG")
        self.__start_at = time.time()
        from concurrent.futures import ThreadPoolExecutor
        thread_steps = []

        try:
            # run step in sequential
            for step in self.teststeps:
                pass
                # if "sync" in step.name():
                #     thread_steps.append(step)
                # else:
                #     #创建线程池 通过map执行self.run_step() 参数列表为processes
                #     with ThreadPoolExecutor(max_workers=10) as executor:
                #         executor.map(self.run_step, thread_steps)
                #         executor.shutdown(wait=True)
                #         thread_steps = []
                        
                #self.run_step(step)
        finally:
            logger.info(f"generate testcase log: {self.__log_path}")
            if ALLURE is not None:
                ALLURE.attach.file(
                    self.__log_path,
                    name="all log",
                    attachment_type=ALLURE.attachment_type.TEXT,
                )

        self.__duration = time.time() - self.__start_at
        return self


class HttpRunner(SessionRunner):
    # split SessionRunner to keep consistent with golang version
    pass



class TestCase(HttpRunner):
    teststeps = []

from httprunner import HttpRunner, Config, Step, RunRequest



testcase = None

def getTestCase(config):
    global testcase
    if testcase is not None:
        return testcase
    else:
        TestCase.config = config
        testcase = TestCase()
        testcase.test_start()
        return testcase

def updateConfig(config):
    global testcase
    TestCase.config = config
    testcase = TestCase()
    testcase.test_start()
    return testcase    

def list_to_dict(li):
    param_dict = {}
    for item in li:
        param_dict[item["name"]] = item["value"]
    return param_dict

def write_yaml(config_dict, steps):
    

    def remove_empty_values_recursively(d):
        if isinstance(d, dict):
            return {k: remove_empty_values_recursively(v) for k, v in d.items() if v not in (None, '', [], {}, '') or (isinstance(v, str) and v.strip())}
        elif isinstance(d, list):
            return [remove_empty_values_recursively(x) for x in d if x not in (None, '', [], {}, '') or (isinstance(x, str) and x.strip())]
        else:
            return d
        
    def format_assert(li):
        assert_list = []
        for item in li:
            assert_func = item["funcName"]
            actual = item["actual"]
            expected = item["expected"]
            msg = item["msg"]
            if msg:
                assert_list.append(
                    {assert_func: [actual, expected, msg]}
                )
            else:
                assert_list.append(
                    {assert_func: [actual, expected]}
                )
        return assert_list

    yaml_dict = {}
    variable_list = config_dict.get("variableList")

    variables_dict = {}
    for variable in variable_list:
        variables_dict[variable["name"]] = variable["value"]

    config_dict["variables"] = list_to_dict(variable_list)
    config_dict = remove_empty_values_recursively(config_dict)
    yaml_dict["config"] = config_dict
    step_list = []
    for step in steps:
        step_dict = {}
        step_dict["name"] = step["name"]
        step_dict["variables"] = list_to_dict(step["variableList"])
        
        step_dict["request"] = {}
        step_dict["request"]["method"] = step["request"]["method"]
        step_dict["request"]["url"] = step["request"]["url"]
        step_dict["request"]["headers"] = list_to_dict(step["request"]["headerList"])
        step_dict["request"]["cookies"] = list_to_dict(step["request"]["cookieList"])

        if step["paramRadio"] == 1:
            step_dict["request"]["data"] = list_to_dict(step["request"]["dataList"])
            
        elif step["paramRadio"] == 2:
            step_dict["request"]["json"] = step["request"]["json"]
        else:
            step_dict["request"]["params"] = list_to_dict(step["request"]["paramList"])
        

        step_dict["extract"] = list_to_dict(step["extractList"])
        step_dict["validate"] = format_assert(step["validateList"])
        step_dict = remove_empty_values_recursively(step_dict)
        print(step_dict)
        step_list.append(step_dict)

    yaml_dict["teststeps"] = step_list
    with open('output.yaml', 'w', encoding="utf8") as file:
        yaml.safe_dump(yaml_dict, file, allow_unicode=True, sort_keys=False)




def parse_step(step):
    casename = step.get("name")
    
    param_type = step.get("paramRadio")
    template = "Step( \n"
    template += f"RunRequest('{casename}')\n"
    cookie_temp = ""
    header_temp = ""

    variables_dict = list_to_dict(step["variableList"])
    if variables_dict:
        template += f'.with_variables(**{variables_dict})\n'

    url = step["request"]["url"]
    method = step["request"]["method"]
    method_temp = f".{method.lower()}('{url}')\n"

    template += method_temp
    
    cookie_list = step.get('cookieList')
    if cookie_list:
        cookies_dict = list_to_dict(cookie_list)
        cookie_temp = f'.with_cookies(**{cookies_dict})\n'

    template += cookie_temp
    headers = step.get('headerList')
    if headers:
        headers_dict = list_to_dict(headers)
        header_temp = f'.with_headers(**{headers_dict})\n'

    template += header_temp

    request_data_temp = ""
    if param_type == 1:
        data_list = step["request"]["dataList"]
        from_data_dict = list_to_dict(data_list)
        request_data_temp = f'.with_data({from_data_dict})\n'
    elif param_type == 2:
        json_data = step["request"]["json"]
        request_data_temp = f".with_json({json_data})\n"
    elif param_type == 3:
        param_list = step["request"]["paramList"]
        param_data_dict = list_to_dict(param_list)
        request_data_temp = f'.with_params(**{param_data_dict})\n'
    
    template += request_data_temp
    template  += ".extract()\n"

    extract_data_temp = ""
    extract_list = step.get('extractList')
    for extract in extract_list:
        data_temp = f'.with_jmespath("{extract["name"]}", "{extract["value"]}")'
        extract_data_temp += data_temp + "\n"
     
    template += extract_data_temp

    template += ".validate()\n"
    validate_list = step.get('validateList')
    validate_data_temp = ""
    for validate in validate_list:
        data_temp = f'.assert_equal("{validate["actual"]}", "{validate["expected"]}", "{validate["msg"]}")'
        validate_data_temp += data_temp + "\n"
    template += validate_data_temp
    template += ")"
    print(template)
    return eval(template)


def get_config(config):
    casename = config.get('name')
    base_url = config.get('base_url')
    is_https = config.get('isHttps')
    variables_dict = {}
    variables = config.get('variableList')
    for variable in variables:
        variables_dict[variable["name"]] = variable["value"]

    exports = config.get('export')
    config_obj = (Config(casename).base_url(base_url).verify(is_https).variables(**variables_dict).export(*exports))
    return config_obj

@app.route("/downloadYaml", methods=["POST"])
def downloadYaml():
    body = request.json
    config = body.get("config")
    steps = body.get("steps")
    try:
        write_yaml(config, steps)
    except Exception as e:
        logger.error(f"执行失败：{str(e)}----------------------------------")
        return jsonify({'status': 'error', 'message':  str(e)}), 400
    file_path = f'./output.yaml'
    logger.info("执行成功----------------------------------------------------")
    return send_file(file_path, as_attachment=True)



@app.route("/updateConfig", methods=["POST"])
def update_config():
    config_dict = request.json
    casename = config_dict["name"]
    base_url = config_dict["base_url"]
    verify = config_dict["verify"]

    variables_dict = {}
    variables = config_dict.get('variableList')
    for variable in variables:
        variables_dict[variable["name"]] = variable["value"]


    exports = config_dict["export"]
    config_obj = (Config(casename).base_url(base_url).verify(verify).variables(**variables_dict).export(*exports))
    
    try:
        updateConfig(config_obj)
    except Exception as e:
        return jsonify({'status': 'error', 'message':  str(e)}), 400
    return jsonify({'status': 'success', 'message': '执行成功'})

@app.route('/runcase', methods=['POST'])
def run_case():
    collected_logs = []

    def collect_logs_handler(message):
        collected_logs.append(message)
    logger.add(collect_logs_handler)
    
    body = request.json
    print(body)
    config = body.get("config")
    steps = body.get("steps")
    config_obj = get_config(config)
    testcase = getTestCase(config_obj)
    print(steps)
    try:
        for step in steps:
            step_obj = parse_step(step)
            testcase.run_step(step_obj)
    except Exception as e:
        logger.error(f"执行失败：{str(e)}----------------------------------")
        return jsonify({'status': 'error', 'message':  str(e), 'logs': collected_logs}), 400
    logger.info("执行成功----------------------------------------------------")
    return jsonify({'status': 'success', 'message': '执行成功', "logs": collected_logs})

@app.route('/runstep', methods=['POST'])
def run_step():
    collected_logs = []

    def collect_logs_handler(message):
        collected_logs.append(message)

    logger.add(collect_logs_handler)
    body = request.json
    config = body.get("config")
    step = body.get("step")
    config_obj = get_config(config)
    testcase = getTestCase(config_obj)
    case_step = parse_step(step)
    try:
        testcase.run_step(case_step)
    except Exception as e:
        logger.error(f"执行失败：{str(e)}----------------------------------")
        return jsonify({'status': 'error', 'message':  str(e), 'logs':collected_logs}), 400
    logger.info("执行成功----------------------------------------------------")
    return jsonify({'status': 'success', 'message': '执行成功', "logs": collected_logs})

from werkzeug.utils import secure_filename
@app.route("/uploadDebugtalk", methods=["POST"])
def uploadDebugtalk():
    # 打印所有上传的文件，以帮助调试
    print(request.files)
    # 尝试获取名为 'debugtalk' 的文件
    file = request.files.get('file')
    # 检查文件是否存在
    if file is None or file.filename != 'debugtalk.py':
        print("文件不存在")
        return jsonify({'status': 'error', 'message': '文件上传错误'}), 400
    
    # 使用 secure_filename 来确保文件名安全
    filename = secure_filename(file.filename)
    file.save(os.path.join('./', filename))
    
    return jsonify({'status': 'success', 'filename': filename}), 200


@app.route('/quit', methods=['GET'])
def quit():
    os._exit(0)
    

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)



